Skip to content

Run Gunicorn on test/prod instead of Django's dev runserver (#1034)#1385

Merged
jonfroehlich merged 1 commit into
masterfrom
1034-gunicorn
Jun 23, 2026
Merged

Run Gunicorn on test/prod instead of Django's dev runserver (#1034)#1385
jonfroehlich merged 1 commit into
masterfrom
1034-gunicorn

Conversation

@jonfroehlich

Copy link
Copy Markdown
Member

Closes #1034.

Swaps Django's dev runserver for Gunicorn on test/prod. The Django docs explicitly warn against runserver in production ("has not gone through security audits or performance tests"), yet we've run it on both test and prod since 2017.

Key point: no UW CSE IT involvement needed

The Django process runs inside our Docker container, behind UW CSE's Apache reverse proxy (127.0.0.1:8571 → container :8000). Swapping the in-container server leaves the port mapping and everything Apache sees identical, so this ships via the normal push-to-deploy path — push to master → test, tag → prod. No Apache edits, no IT ticket.

One thing worth confirming with @mechanicjay (verification only): that Apache serves /static/ and /media/ from the filesystem rather than proxying them to the container. Almost certainly yes — prod runs DEBUG=False, and runserver refuses to serve static when DEBUG=False, yet our CSS/JS work in prod.

Changes

  • requirements.txt: add gunicorn==23.0.0.
  • docker-entrypoint.sh: start Gunicorn when DJANGO_ENV is TEST/PROD; keep runserver for local dev (DJANGO_ENV=DEBUG) so auto-reload, the debug toolbar, and DEBUG=True static serving still work. Uses exec so the server becomes PID 1 and receives Docker's stop signals.

Tuning (overridable via env vars)

  • GUNICORN_WORKERS defaults to 3 — the (2*cores)+1 rule of thumb would be ~49 on the 24-core host, but that box is shared with all Project Sidewalk instances (Makeability Lab website slow to load #959), so we stay conservative.
  • GUNICORN_TIMEOUT defaults to 120s — Gunicorn's default 30s could kill slow ImageMagick/PDF thumbnail generation.

Why this is not a latency fix

The #959 slowness was N+1 queries (since fixed via the ORM). Gunicorn won't speed up a single render; it makes serving production-grade (security, managed workers, crash-restart, request timeouts).

Test plan

  1. Merge to master → auto-deploys to makeabilitylab-test.
  2. On -test: browse pages; save an admin record with an image upload (exercises the 120s timeout path); confirm /static/ and /media/ still load.
  3. If clean, tag a release for prod. Trivially reversible — revert two lines, redeploy.

🤖 Generated with Claude Code

Django's `runserver` is a development server the docs explicitly warn
against in production ("has not gone through security audits or
performance tests"). We had been running it on both test and prod since
2017. Swap it for Gunicorn, the recommended WSGI server.

The change is entirely inside the container: Apache still reverse-proxies
dynamic requests to 127.0.0.1:8571 and serves /static/ and /media/
directly, so this ships via the normal push-to-deploy path with no
Apache/UW CSE IT changes.

- requirements.txt: add gunicorn==23.0.0
- docker-entrypoint.sh: start Gunicorn when DJANGO_ENV is TEST or PROD;
  keep `runserver` for local dev (DJANGO_ENV=DEBUG) so auto-reload, the
  debug toolbar, and DEBUG=True static serving still work. Workers (3)
  and timeout (120s) default conservatively for the shared host and are
  overridable via GUNICORN_WORKERS / GUNICORN_TIMEOUT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jonfroehlich jonfroehlich merged commit a8fbfa7 into master Jun 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shift production server off of runserver?

1 participant